package dancrawler.client;

import dancrawler.common.Misc;
import dancrawler.client.gamecmd.ClientCmdRegister;
import dancrawler.proto.cmd.CmdHandlerRegister;
import dancrawler.server.Connection;
import dancrawler.server.ProtocolParser;
import dancrawler.server.netMsg;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import sun.rmi.transport.proxy.CGIHandler;

/**
 * A <i>Client</i> is used for all communication with the gameserver.
 */
public class Client {

    private class ClientReadThread extends Thread {

        Client cl;

        @Override
        public void run() {
            ProtocolParser parser = new ProtocolParser();
            try {
                InputStream ins = cl.socket.getInputStream();
                byte[] idata = new byte[1024];
                while (true) {
                    int n = ins.read(idata);
                    if (n == -1) {
                        throw new IOException();
                    }
                    ByteBuffer buff = ByteBuffer.wrap(idata);
                    buff.position(0);
                    buff.limit(n);
                    while (buff.remaining() != 0) {
                        parser.processData(buff, serverCon);
                    }
                    netMsg msg = null;
                    while ((msg = serverCon.popMsg()) != null) {
                        if (cl.hreg.lookupHandler(msg.getCmd()) == null) {
                            System.out.println("CMD ER NULL " + msg.getCmd());
                        }
                        cl.hreg.lookupHandler(msg.getCmd()).handleCmd(msg);

                    }
                    for (GameListener gli : cl.glis) {
                        gli.worldUpdate();
                    }
                }



            } catch (IOException ex) {
                try {
                    cl.socket.close();
                } catch (IOException ex1) {
                }
                cl.cwt.interrupt();
            }

        }
    }

    private class ClientWriteThread extends Thread {

        Client cl;

        @Override
        public void run() {
            OutputStream ons = null;
            try {
                ProtocolParser parser = new ProtocolParser();
                ons = cl.socket.getOutputStream();

                byte[] outb = new byte[1024];
                ByteBuffer obuff = ByteBuffer.allocate(1024);
                while (true) {
                    netMsg msg = writeQueue.take();//writeQueue.poll(1001, TimeUnit.MILLISECONDS);
                    if (msg == null) {
                        msg = new netMsg((short) 11, (short) 0, serverCon);
                        msg.setFinalized(true);
                    }
                    obuff.clear();
                    parser.writeMsg(obuff, msg);
                    obuff.flip();
                    obuff.get(outb, 0, obuff.limit());
                    ons.write(outb, 0, obuff.limit());

                }
            } catch (InterruptedException | IOException ex) {
            } finally {
                try {
                    ons.close();
                    cl.socket.close();
                } catch (IOException ex) {
                }
                for (GameListener gli : cl.glis) {
                    gli.gameEnded();
                }
            }
        }
    }

    /**
     * ClientRec is used by the client commands handlers to update local state
     * in response to network commands.
     */
    public class ClientRec {

        private ClientRec() {
        }

        public void addPlayer(String name, short id) {
            players.put(new Integer(id), name);
            for (GameListener gli : glis) {
                gli.playerUpdate();
            }
        }

        public void rmPlayer(short id) {
            players.remove(new Integer(id));
            for (GameListener gli : glis) {
                gli.playerUpdate();
            }
        }

        public void chatRec(int player, String msg) {
            for (GameListener gli : glis) {
                gli.chatMsg(players.get(player), msg);
            }
        }

        public void playerDeath() {
            for (GameListener gli : glis) {
                gli.die();
            }
        }

        public void defTile(String name, int id, boolean blocks) {
            tiles.put(id, new Tile(0, 0, name, blocks));
        }

        public Tile getTile(int id) {
            return tiles.get(id);
        }

        public void setViewRange(int range) {
        }
        public void clearMap()
        {
            world = new ClientWorld();
        }
    }
    private ArrayList<GameListener> glis = new ArrayList<>();
    private Connection serverCon = new Connection();
    private ClientReadThread crt = new ClientReadThread();
    private ClientWriteThread cwt = new ClientWriteThread();
    private InetSocketAddress addr;
    private Socket socket;
    private BlockingQueue<netMsg> writeQueue = new LinkedBlockingQueue<>();
    private ClientWorld world;
    private GameObjectRegister reg = new GameObjectRegister();
    private CmdHandlerRegister hreg = new CmdHandlerRegister();
    private short protover = (short) 10;
    private HashMap<Integer, String> players = new HashMap<>();
    private ClientRec rec = new ClientRec();
    private String name;
    private int focus;
    private boolean focusSet = false;
    private Map<Integer, Tile> tiles = new Hashtable<>();

    /**
     *
     * @param name The display-name of the local player
     */
    public Client(String name) {
        ClientCmdRegister.registerHandlers(hreg, this);
        world = new ClientWorld();
        this.name = name;
    }

    /**
     * Transmits a message to the server.
     *
     * @param msg The message to be transmitted
     */
    public void writeMsg(netMsg msg) {
        writeQueue.offer(msg);
    }

    /**
     * Establishes a connection to the server
     *
     * @param host The hostname of the server
     * @param port The port to connect to
     * @throws UnknownHostException
     * @throws IOException
     */
    public void connect(String host, int port) throws UnknownHostException, IOException {

        socket = new Socket(host, port);
        socket.setTcpNoDelay(true);
        //socket.setSoTimeout(7000);
        //socket.setKeepAlive(true);
        crt.cl = this;
        cwt.cl = this;
        cwt.setDaemon(true);
        crt.setDaemon(true);
        crt.start();
        cwt.start();
        netMsg msg = new netMsg((short) 13, (short) 0, serverCon);
        byte[][] args = new byte[1][];
        msg.setArgs(args);
       
        
        try {
            byte[] b = name.getBytes("UTF-8");
            args[0] = new byte[b.length + 1];
            args[0][0] = (byte) b.length;
            System.arraycopy(b, 0, args[0], 1, b.length);
        } catch (UnsupportedEncodingException ex) {
            return;
        }
        msg.setFinalized(true);
        writeQueue.offer(msg);




    }

    /**
     * Disconnects the client
     */
    public void stopClient() {
        try {
            socket.close();
        } catch (IOException ex) {
        }
    }

    /**
     * Issues a move order, attempting to move, if possible, in the given
     * direction
     *
     * @param direction Direction to move,
     * @see dancrawler.client.Gameworld#moves
     * @see dancrawler.client.Gameworld#directions
     */
    public void move(int direction) {
        netMsg msg = null;
        switch (direction) {
            case Gameworld.NORTH:
                msg = new netMsg((short) 3, (short) 0, serverCon);
                break;
            case Gameworld.WEST:
                msg = new netMsg((short) 2, (short) 0, serverCon);
                break;
            case Gameworld.SOUTH:
                msg = new netMsg((short) 5, (short) 0, serverCon);
                break;
            case Gameworld.EAST:
                msg = new netMsg((short) 4, (short) 0, serverCon);
                break;
            case 0:
                msg = new netMsg((short) 23, (short) 0, serverCon);
                break;
            default:
                return;
        }
        msg.setArgs(new byte[0][]);
        msg.setFinalized(true);
        writeQueue.offer(msg);
    }

    /**
     * Orders an attack at a given angle
     *
     * @param angle The angle between the playerCharacter and the target. A full
     * circle is 255 degrees, increasing counterclockwise
     */
    public void attack(byte angle) {
        netMsg msg = new netMsg((short) 18, (short) 0, serverCon);
        byte[][] args = new byte[1][];
        args[0] = new byte[]{angle};
        msg.setArgs(args);
        writeQueue.offer(msg);

    }

    /**
     * Adds a listener that is to be notified of game events
     *
     * @param gli
     */
    public void addGameListener(GameListener gli) {
        this.glis.add(gli);
    }

    /**
     *
     * @return The local representation of the game world
     */
    public ClientWorld getWorld() {
        return world;
    }

    public GameObjectRegister getObjectRegister() {
        return reg;
    }

    public Collection<String> getPlayers() {
        return players.values();
    }

    public ClientRec getRec() {
        return rec;
    }

    public int getFocusX() {
        GameObject obj = reg.getGameObject(focus);
        if (obj == null || !focusSet) {
            return -1;
        }
        return obj.getX();
    }

    public int getFocusY() {
        GameObject obj = reg.getGameObject(focus);
        if (obj == null || !focusSet) {
            return -1;
        }
        return obj.getY();
    }

    public void setFocus(int id) {
        focus = id;
        focusSet = true;
    }

    public void sendChat(String text) {
        netMsg msg = new netMsg((short) 16, (short) 0, null);
        System.out.println(text);
        byte[][] args = new byte[1][];
        args[0] = Misc.stringToBytes(text);
        msg.setArgs(args);
        writeQueue.offer(msg);
    }
}
